<?php

/*
 * Парсер YAML в структуры PHP
 * Использует класс \T4\Standard вместо \stdClass
 * В случае если в файле YAML содержится более, чем один документ,
 * используется класс \T4\Collection для коллекции объектов, а не массив
 * 
 */

namespace T4\YAML;

use T4\Standard;

use T4\Exception;

class Parser {
	
	/**
	 * Разделитель "документов" 
	 */
	const DOCUMENT_DELIMITER = '---';
	
	/**
	 * Паттерны токенов
	 */
	private $patterns;
	
	public function __construct() {
		$this->patterns = array(
			'NEW_DOCUMENT'		=> '/^' . self::DOCUMENT_DELIMITER . '/',
			'COMMENT'			=> '/^\s*##/',
			'ARRAY_OF_OBJECTS'	=> '/^\s*[-]\s+(\S+)\s*\:\s+?([\s\S]*)$/',
			'ARRAY'				=> '/^\s*[-]\s+([\s\S]+)$/',
			'OBJECT'			=> '/^\s*(\S+?)\s*\:\s+(\S+)/',
			'OBJECT_MULTILINE'	=> '/^\s*(\S+?)\s*\:\s*/'
		);
	}
	
	public function parse($yaml, $class=null) {
		if ( is_null($class) )
			$class = '\\T4\Standard';
		return new $class($this->parseAsArray($yaml));
	}
	
	public function parseAsArray($yaml) {
		
		if ( empty($yaml) )
			return array();
				
		if ( !is_array($yaml) )
			$yaml = $this->prepareAsArray($yaml);
				
		$document = array();
		
		$pass = 0;
		
		foreach ( $yaml as $num => $str ) {
			
			// Пропуск заданного количества строк
			// для обработки блоков
			while ( $pass > 0 ) {
				$pass--;
				continue 2;
			}
			
			switch ( true ) {
				
				// Начало нового документа
				// @todo Пока не реализовано несколько документов в одном файле
				case preg_match($this->patterns['NEW_DOCUMENT'], $str['string']):
					$document = array();
					break;
					
				// Комментарий на всю строку
				case preg_match($this->patterns['COMMENT'], $str['string']):				
					continue;
					break;
				
				// Начало последовательности сопоставлений
				case preg_match($this->patterns['ARRAY_OF_OBJECTS'], $str['string'], $matches):
					$block = $this->getSubBlock($yaml, $num);
					if ( count($block) > 0 ) {
						$new = $this->parseAsArray($block);
						$pass = count($block);
					}
					$new[$matches[1]] = $matches[2];
					$document[] = $new;
					break;
				
				// Простая последовательность (одна строка)
				case preg_match($this->patterns['ARRAY'], $str['string'], $matches):
					$document[] = $matches[1];
					break;
				
				// Простое сопоставление (одна строка)
				case preg_match($this->patterns['OBJECT'], $str['string'], $matches):
					$document[$matches[1]] = $matches[2];
					break;
				
				// Сопоставление с блоком строк (или сопоставление с пустым значением)
				case preg_match($this->patterns['OBJECT_MULTILINE'], $str['string'], $matches):
					$block = $this->getSubBlock($yaml, $num);
					if ( count($block) > 0 ) {
						$document[$matches[1]] = $this->parseAsArray($block);
						$pass = count($block);
					} else {
						$document[$matches[1]] = '';
					}
					break;
					
				default:
					throw new Exception('Не распознана структура YAML: ' . $str['string']);
					
			}
			
		}

		return $document;
		
	}
	
	private function prepareAsArray($yaml) {
		$yaml = explode("\n", $yaml . "\n");
		$ret = array();
		foreach ( $yaml as $str ) {
			$str = rtrim($str);
			if ( empty($str) )
				continue;
			$ret[] = array('ident' => $this->getIdent($str), 'string' => $str);
		}
		return $ret;
	}
	
	private function getIdent($str) {
		return strspn($str, ' ');
	}
	
	private function getSubBlock($yaml, $num) {
		$ident = $yaml[$num]['ident'];
		$tail = array_slice($yaml, $num+1);
		$ret = array();
		foreach ( $tail as $str ) {
			if ( $str['ident'] <= $ident )
				break;
			$ret[] = $str;
		}
		return $ret;
	}
	
}
